[기초부터 완성까지 프론트엔드 4장]

4장

arguments

화살표 함수를 제외한 모든 함수에서는 arguments라는 객체를 사용할 수 있습니다. arguments 객체를 사용하여 함수에 실제로 전달된 인자들을 참조할 수 있습니다. 또한 arguments 객체는 유사배열 객체이기 때문에 인덱스로 프로퍼티에 접근할 수 있으며, length 프로퍼티를 가지고 있습니다.

function sum(x, y, z) {
  console.log(arguments[0]) // 1
  console.log(arguments[1]) // 2
  console.log(arguments[2]) // undefined
  console.log(arguments.length) // 2

  return x + y + z
}

sum(1 + 2)

arguments 객체는 ES2015에서 등장한 나머지 매개변수로 대체할 수 있습니다. 나머지 매개변수는 유사배열 객체가 아닌 진짜 배열이기 때문에 인자들을 배열로 다루고 싶은 경우 유용하게 사용할 수 있습니다.

function sum(...args) {
  arg.forEach(function(arg) {
    // ...
  })
}

sum(1, 2)

this

자바스크립트 함수에서는 this라는 특별한 키워드를 사용할 수 있습니다. this는 읽기 전용 값으로 런타임 시 설정할 수 없으며 함수를 호출한 방법에 의해 값이 달라집니다.

일반 함수를 설명하기 앞서 먼저 전역 실행 컨텍스트에서의 this 바인딩을 보겠습니다. 전역 실행 컨텍스트에서의 this는 항상 전역 객체를 참조합니다. 전역 실행 컨텍스트는 자바스크립트 엔진이 코드를 실행할 때 처음으로 생성되는 컨텍스트입니다. 자바스크립트 코드가 실행되는 최상위 환경이라고 할 수 있습니다.

전역 객체는 자바스크립트를 실행하는 환경마다 다릅니다. 브라우저 환경에서 자바스크립트를 실행한다면 window 객체가 되며, Node.js 환경에서 실행하면 global 객체가 전역 객체가 됩니다.
function func() {
  console.log(this === window) // true
}
func()

func() 함수 호출 시에도 마찬가지로 this의 값이 전역객첼르 참조합니다. 하지만 this는 window 전역객체를 참조하는 것이 아니라 undefined로 남아 있어야 합니다. this가 바인딩되지 않은 경우 기본값으로 전역 객체를 참조하기 때문에 예제 코드와 같은 경과가 나옵니다. 이 문제는 “use strict” 지시문을 사용하여 엄격 모드를 사용하면 해결할 수 있습니다.

function func() {
  'use strict'
  console.log(this === window) //false
  console.log(this === undefined) //true
}

생성자 함수

new 키워드를 사용하여 함수를 호출하면 생성자 함수로 동작합니다. 생성자 함수의 this 바인딩은 일반 함수 호출과는 다르게 동작합니다.

function Vehicle(type) {
  this.type = type
}
const car = new Vehicle('Car')

생성자 함수를 호출하여 객체가 생성될 때 아래와 같은 단계로 동작합니다.

객체를 생성하여 this에 바인딩

생성자 함수 내의 코드를 실행하기 전에 객체를 만들어 this에 바인딩합니다. 생성된 객체는 생성자 함수의 prototype 프로퍼티에 해당하는 객체를 프로토타입으로 설정합니다 ??? 무슨말이지?

프로퍼티 생성

function Vehicle(type) {
  this.type = type // 프로퍼티 생성
}

객체 반환

생성된 객체, this에 바인딩한 객체를 반환합니다. 또한 반환 값을 따로 명시하지 않아도 this에 바인딩한 객체가 반환됩니다. 다만, this가 아닌 다른 반환 값을 명시적으로 지정하였다면 this가 아닌 해당 값이 반환됩니다.

function Vehicle(type) {
  this.type = type
  return this // 이 부분을 생략하여도 this에 바인딩한 객체가 반환됩니다.
}

이러한 과정은 반드시 new 키워드와 함께 생성자 함수를 호출한 경우에만 실행됩니다.

메서드

자바스크립트에서는 객체의 프로퍼티인 함수를 일반 함수와 구분하여 메서드라고 부르며, this바인딩도 일반 함수와는 다르게 동작합니다. 메서드를 호출하면 this는 해당 메서드를 소유하는 객체로 바인딩됩니다.

const obj = {
  lang: 'javascript',
  greeting() {
    // this가 obj 객체로 바인딩됩니다.
    console.log(this)
    return `hello ${this.lang}`
  },
}
console.log(obj.greeting()) // "hello javascript"

여기서 중요한 것은 메서드를 어떻게 호출했느냐에 따라 this 바인딩이 달라진다는 것입니다.

const obj = {
  lang: 'javascript',
  greeting() {
    return `hello ${this.lang}`
  },
}
const greeting = obj.greeting

console.log(greeting()) // "hello undefined"

내가 이해한 것이 맞다면 this는 호출한 함수 또는 메소드의 바로 위 객체를 가르킨다.

call(), apply(), bind()

자바스크립트에서는 함수의 내장 메서드인 call(), appley(), bind() 메서드를 이용하여 this로 바인딩될 객체를 변경할 수 있습니다. 그리고 이러한 방법을 명시적 바인딩이라고 부릅니다.

call()과 apply() 메서드는 어떤 함수를 다른 객체의 메서드처럼 호출할 수 있게 합니다. 두 메서드는 넘겨받는 인자의 형식만 다를뿐, this를 특정 객체에 바인딩하여 함수를 호출하는 역할을 합니다.

const obj = { name: 'javascript' }

function greeting() {
  return `Hello ${this.name}`
}

console.log(greeting.call(obj)) // Hello javascript

apply()는 call()과 비슷하지만 함수에 전달한 인자들을 배열 형태로 전달해야 합니다.

const obj = { name: 'Lee' }

function getUserInfo(age, country) {
  return `name: ${this.name}, age: ${age}, country: ${country}`
}

console.log(getUserInfo.apply(obj, [20, 'korea'])) // 'name: Lee, age: 20, country: Korea'

bind()

함수의 this 바인딩을 변경할 수 있는 또 다른 방법으로 bind() 메서드를 사용하는 방법이 있습니다. 앞서 살펴본 call()과 apply() 메서드와는 두 가지 차이점이 있습니다.

  • bind() 메서드는 함수의 this 바인딩을 영구적으로 변경합니다. (생성자 함수로 사용되는 경우는 예외), bind() 메서드로 this가 변경된 함수는 call(), apply() 또는 다른 bind() 메서드를 사용해도 this 바인딩을 변경할 수 없습니다.
  • this를 바인딩하여 함수를 호출하는 것이 아니라 새로운 함수를 반환합니다.

즉 bind() 함수는 함수가 어디서 어떻게 호출되는지 상관없이 this 값을 고정하고 싶을 때 사용합니다.

const obj1 = { name: 'Lee' }
const obj2 = { name: 'Han' }

function getUserInfo(age, country) {
  return `name: ${this.name}, age: ${age}, country: ${country}`
}

const bound = getUserInfo.bind(obj1)

console.log(bound(20, 'korea')) // 'name: Lee, age: 20, country: Korea'
console.log(bound.apply(obj2, [20, 'korea'])) // 'name: Lee, age: 20, country: Korea'

필자는 this를 사용하면서 call(), apply(), bind()을 사용해본적이 없다. 실제로 위에 메소드들을 사용하지 않고 화살표함수로 문제를 해결했었다. 필요가 있어서 나온 메소드지만, 내가 경험한 코드가 적어서 그런건지 보기 좀 불편해서 사용하지 않았다.

화살표 함수와 렉시컬 this

화살표 함수의 this는 화살표 함수를 둘러싸고 있는 렉시컬 스코프에서 this의 값을 받아 사용합니다. 이러한 this를 렉시컬 this라고 하며 이 값은 변경되지 않습니다.

렉시컬 스코프는 자바스크립트 엔진이 변수를 찾는 검색 방식에 대한 규칙이며, 함수를 어디에 선언했는지에 따라 결정됩니다.

const obj = {
  lang: 'javascript',
  greeting: () => {
    return `hello ${this.lang}`
  },
}
console.log(obj.greeting()) // 'hello undefined'

화살표 함수의 렉시컬 this가 obj가 아닌 obj를 둘러싸고 있는 전역 컨텍스트에서 값을 받아옵니다. 즉 전역객체를 this가 가리키기 때문입니다.

화살표 함수는 this를 따로 바인딩하지 않고 변경되지 않는 렉시컬 this를 갖습니다. 이러한 특징때문에 화살표 함수의 this는 call(), apply(), bind() 함수를 사용하여 변경할 수 없습니다.

화살표 함수는 정적인 렉시컬 this를 사용하기 때문에 기존의 동적인 this 바인딩의 혼잡함에서 벗어나 단순하게 사용할 수 있습니다.

const obj = {
  name: 'javascript',
  greeting() {
    setTimeout(() => {
      console.log(this.name) // 'javascript'
    }, 1000)
  },
}